【Android】 パーミッション判定の複雑なコードを解決!PermissionsDispatcherライブラリを試してみた
はじめに
モバイルアプリサービス部の浜田です!
AndroidアプリのRuntime Permissionの対応を行ったときに、パーミッションの状態を判定するコードがあふれてしまって、収拾がつかなくなった経験はありますか?
今回は、パーミッションの状態に応じて処理を実行してくれる「PermissionsDispatcher」というライブラリを試してみたので、使用方法などをまとめておきます。
PermissionsDispatcherライブラリとは
PermissionsDispatcherは、AndroidアプリのRuntime Permission対応のためのライブラリです。このライブラリが提供してくれるアノテーションのおかげで、パーミッションの状態に対する処理が宣言的に実装できます。
アノテーション | 必須 | 概要 |
---|---|---|
@RuntimePermissions |
✓ | パーミッションを処理するActivity やFragment に付ける |
@NeedsPermission |
✓ | パーミッションが必要な処理を実行する関数に付ける |
@OnShowRationale |
ユーザーにパーミッションが必要な理由を説明するときに呼び出される関数に付ける | |
@OnPermissionDenied |
ユーザーがパーミッションが付与しなかったときに呼び出される関数に付ける | |
@OnNeverAskAgain |
ユーザーがパーミッションの要求ダイアログで「今後表示しない」を選択したときに呼び出される関数に付ける |
環境
Android Studio
Android Studio 3.2.1 Build #AI-181.5540.7.32.5056338, built on October 9, 2018 JRE: 1.8.0_152-release-1136-b06 x86_64 JVM: OpenJDK 64-Bit Server VM by JetBrains s.r.o macOS 10.14
動作検証端末
- エミュレータ
- Pixel
- Android 9.0 (API 28) x86
使用するライブラリのバージョン
2018年10月20日にPermissionsDispatcherライブラリのバージョン4.0.0
がリリースされていますが、README.md
に記載されているとおり、Jetpackの環境のみサポートしています。
NOTE: 4.x is still alpha and it only supports Jetpack. If you use appcompat 3.x which is almost stable is the way to go.
この記事では非Jetpackの環境を対象として、以下のドキュメントを参考にバージョン3.3.1
を使用します。
- PermissionsDispatcher/README.md at 3.3.1 · permissions-dispatcher/PermissionsDispatcher
- PermissionsDispatcher/kotlin_support.md at 3.3.1 · permissions-dispatcher/PermissionsDispatcher
実装例
1. 新規プロジェクトの作成
まず、Android Studioを使用して新規プロジェクトを作成します。 以下の設定で、アプリケーション名などは任意のものを指定してください。
Create Android Project
:include Kotlin support
: ON
Target Android Devices
:Phone and Tablet
: ON
Add an Activity to Mobile
:Empty Activity
を選択
Configure Activity
:Activity Name
:MainActivity
を入力Generate Layout File
: ON
Layout Name
:activity_main
を入力Backwards Compatibility (AppCompat)
: ON
2. ファイルの内容を変更
新規プロジェクトの作成後、以下のファイルを変更します。
app
モジュールのbuild.gradle
AndroidManifest.xml
activity_main.xml
MainActivity.kt
appモジュールのbuild.gradle
PermissionsDispatcherライブラリを使用するために、app
モジュールのbuild.gradle
に以下の内容を追加します。
apply plugin: 'kotlin-kapt' dependencies { implementation("com.github.hotchemi:permissionsdispatcher:3.3.1") { // Fragmentで使用したい場合はこの記述を外す exclude module: "support-v13" } kapt "com.github.hotchemi:permissionsdispatcher-processor:3.3.1" }
AndroidManifest.xml
例として、MainActivity.kt
に連絡先の登録数を表示する処理を実装します。
連絡先の取得にはREAD_CONTACTS
パーミッションが必要なので、AndroidManifest.xml
に以下の内容を追加しておきます。
<uses-permission android:name="android.permission.READ_CONTACTS" />
activity_main.xml
パーミッションが必要な処理を実行するボタンを設置します。
ボタンがタッチされたときの処理をMainActivity.kt
で設定するために、android:id
に@+id/button
を指定しています。
<?xml version="1.0" encoding="utf-8"?> <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <Button android:text="連絡先の登録数を表示" android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/button" app:layout_constraintTop_toTopOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" /> </android.support.constraint.ConstraintLayout>
こんな感じの単純なレイアウトです。
MainActivity.kt
MainActivity
を以下の内容に変更してください。
@RuntimePermissions class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) button.setOnClickListener { // 自動生成された関数にパーミッションが必要な処理の呼び出しを委譲 showContactsWithPermissionCheck() } } override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) // 自動生成された関数にパーミッション・リクエストの結果に応じた処理の呼び出しを委譲 onRequestPermissionsResult(requestCode, grantResults) } /** * 連絡先の登録数をトーストで表示する。 */ @NeedsPermission(Manifest.permission.READ_CONTACTS) fun showContacts() { contentResolver.query( ContactsContract.Contacts.CONTENT_URI, null, null, null, null ).use { Toast.makeText(this@MainActivity, "登録数: ${it.count}", Toast.LENGTH_SHORT).show() } } @OnPermissionDenied(Manifest.permission.READ_CONTACTS) fun onContactsDenied() { Toast.makeText(this, "「許可しない」が選択されました", Toast.LENGTH_SHORT).show() } @OnShowRationale(Manifest.permission.READ_CONTACTS) fun showRationaleForContacts(request: PermissionRequest) { AlertDialog.Builder(this) .setPositiveButton("許可") { _, _ -> request.proceed() } .setNegativeButton("許可しない") { _, _ -> request.cancel() } .setCancelable(false) .setMessage("登録数を取得するために連絡先にアクセスする必要があります。") .show() } @OnNeverAskAgain(Manifest.permission.READ_CONTACTS) fun onContactsNeverAskAgain() { Toast.makeText(this, "「今後表示しない」が選択されました", Toast.LENGTH_SHORT).show() } }
実装の解説
1. @RuntimePermissionsアノテーション
パーミッションを処理するActivity
に@RuntimePermissions
アノテーションを付けます。
@RuntimePermissions class MainActivity : AppCompatActivity() { // ... }
2. @NeedsPermissionアノテーション
パーミッションが必要な処理を実行する関数に@NeedsPermission
アノテーションを付けます。
@NeedsPermission(Manifest.permission.READ_CONTACTS) fun showContacts() { // ... }
3. @OnShowRationaleアノテーション
Androidの公式ドキュメントで次のように解説されています。
実行時のパーミッション リクエスト | Android Developers
ユーザーがパーミッションを必要とする機能を使おうとしながら、パーミッション リクエストを拒否し続けている場合、ユーザーは、その機能を利用するにはアプリにパーミッションが必要であることを理解していない可能性があります。このような場合は、ユーザーに対して説明を表示するとよいでしょう。
このような状況を判断するために、ActivityCompat.shouldShowRequestPermissionRationale()
という関数がサポートライブラリで提供されており、この関数がtrue
を返す状況で、OnShowRationale
アノテーションを付けた関数が呼び出されます。
@OnShowRationale(Manifest.permission.READ_CONTACTS) fun showRationaleForContacts(request: PermissionRequest) { // ... }
4. @OnPermissionDeniedアノテーション
ユーザーがパーミッションが付与しなかったときに呼び出したい関数に@OnPermissionDenied
アノテーションを付けます。
@OnPermissionDenied(Manifest.permission.READ_CONTACTS) fun onContactsDenied() { // ... }
5. @OnNeverAskAgainアノテーション
ユーザーがパーミッションの要求ダイアログで「今後表示しない」を選択したときに呼び出される関数に@OnNeverAskAgain
アノテーションを付けます。
@OnNeverAskAgain(Manifest.permission.READ_CONTACTS) fun onContactsNeverAskAgain() { // ... }
6. 処理の委譲
アノテーションの指定に対して、PermissionsDispatcherライブラリが次のような関数やクラスを裏で生成してくれます。
fun MainActivity.showContactsWithPermissionCheck() { // ... } fun MainActivity.onRequestPermissionsResult(requestCode: Int, grantResults: IntArray) { // ... } private class MainActivityShowContactsPermissionRequest(target: MainActivity) : PermissionRequest { // ... override fun proceed() { // ... } override fun cancel() { // ... } }
MainActivity.showContactsWithPermissionCheck()
は@NeedsPermission
アノテーションを付けたshowContacts()
に対して生成された拡張関数です。showContacts()
を直接呼び出す代わりに、この生成された関数を呼び出すことで、パーミッションが付与されている場合は処理を実行し、パーミッションが付与されていない場合は状況に応じて適切な処理を呼び出してくれます。
使用例では、ボタンがタッチされたときにこの関数を呼び出しています。
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) button.setOnClickListener { // 自動生成された関数にパーミッションが必要な処理の呼び出しを委譲 showContactsWithPermissionCheck() } }
MainActivity.onRequestPermissionsResult(requestCode: Int, grantResults: IntArray)
はパーミッション・リクエストの結果を委譲するための拡張関数です。
使用例では、オーバーライドしたonRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray)
の中で単純に委譲しています。
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) // 自動生成された関数にパーミッション・リクエストの結果に応じた処理の呼び出しを委譲 onRequestPermissionsResult(requestCode, grantResults) }
MainActivityShowContactsPermissionRequest
クラスは、@OnShowRationale
アノテーションを付けたshowRationaleForContacts()
に対して生成されたものです。
@OnShowRationale(Manifest.permission.READ_CONTACTS) fun showRationaleForContacts(request: PermissionRequest) { AlertDialog.Builder(this) .setPositiveButton("許可") { _, _ -> request.proceed() } .setNegativeButton("許可しない") { _, _ -> request.cancel() } .setCancelable(false) .setMessage("登録数を取得するために連絡先にアクセスする必要があります。") .show() }
@OnShowRationale
アノテーションを付けた関数の引数にPermissionRequest
オブジェクトが渡されます。
proceed()
メソッドの呼び出しで、アノテーションの引数で指定しているパーミッションの再要求が実行され、cancel()
メソッドの呼び出しで、@OnPermissionDenied
アノテーションが付けられた関数が実行されます。
動作確認
パーミッションが付与されているときの動作
パーミッションが付与されている状況でボタンがタッチされたときはすぐにshowContacts()
が実行され、連絡先の登録数がトーストで表示されます。
パーミッションが付与されていないときの動作
インストール後初めてのパーミッション・リクエスト
パーミッションが付与されていない状況で、初めてボタンがタッチされたときは次のようなダイアログがOSによって表示されます。
「許可」が選択された場合:
パーミッションが付与され、すぐにshowContacts()
が実行され、連絡先の登録数がトーストで表示されます。
「許可しない」が選択された場合:
パーミッションは付与されず、@OnPermissionDenied
アノテーションを付けた関数が実行されます。
一度拒否されたあとのパーミッション・リクエスト
さきほどのダイアログで「許可しない」を選択したあとで、もう一度ボタンをタッチすると、@OnShowRationale
アノテーションを付けた関数が実行され、例として独自に実装しているダイアログが表示されます。
「許可」が選択された場合:
パーミッション・リクエストが実行され、OSによってダイアログが表示されます(初回と異なり、「今後表示しない」というチェックボックスが追加されます)。
「許可しない」が選択された場合:
パーミッションは付与されず、@OnPermissionDenied
アノテーションを付けた関数が実行されます。
「今後表示しない」が選択された場合
OSによって表示されたパーミッション・リクエストのダイアログで、ユーザーが「今後表示しない」を選択した場合、それ以降は常に@OnNeverAskAgain
アノテーションを付けた関数が実行されます。
Androidの公式ドキュメントに記載されているとおり、この状況でアプリがパーミッション・リクエストを実行することはできません。
If the user checks the Never ask again box and taps Deny, the system no longer prompts the user if you later attempt to requests the same permission.
この状況で、パーミッションが必要な機能の使用をユーザーが試みたときは、設定アプリから手動でパーミッションを許可するように促すと良いです。
さいごに
以前、サポートライブラリのAPIを直接使用してRuntime Permission対応したときに、パーミッションの状態に対して実行される処理が不明確なコードを書いてしまったことがあります…。
PermissionsDispatcherライブラリを使用することで、ユーザーへの説明など、本質的な仕様の検討に集中できる感触がありました。ぜひ、みなさんも試してみてください!
参考文献
- GitHub - permissions-dispatcher/PermissionsDispatcher: Simple annotation-based API to handle runtime permissions.
- Permissions overview | Android Developers
- 実行時のパーミッション リクエスト | Android Developers
- App permissions best practices | Android Developers
- Runtime Permissionsに対応するPermissionsDispatcherをリリースしました - hotchemi-ja-blog
- いまからはじめるAndroid 6.0対応 - Speaker Deck
- あらためてRuntime Permissionと実装方法をおさらいする - Qiita
- Request Permissionの実装と、UX(ユーザー印象)に気をつけようねという話 - Qiita
- 5分で対応するMパーミッション - Qiita
- Android6.0 RuntimePermissionの実装と注意点